一、自定义 Webpack Loader
Loader 是 Webpack 处理非 JavaScript 模块的核心机制,本质是一个函数,用于将输入的模块代码转换为输出代码。
1.1 Loader 基础实现
目标:创建一个替换源码中特定字符串的 Loader。
创建 Loader 文件(
./loader/replaceLoader.js):// 必须使用普通函数(箭头函数会丢失 this 上下文) module.exports = function(source) { // source 为输入的模块源码 console.log('原始源码:', source); // 将 "kkb" 替换为 "222" return source.replace('kkb', '222'); };在 Webpack 中配置使用:
const path = require('path'); module.exports = { module: { rules: [ { test: /\.js$/, // 匹配所有 JS 文件 use: { // 指定 Loader 路径 loader: path.resolve(__dirname, './loader/replaceLoader.js') } } ] } };
1.2 接收配置参数
通过 loader-utils 工具获取配置参数,使 Loader 更灵活。
安装依赖:
npm install loader-utils -D改进 Loader:
const loaderUtils = require('loader-utils'); module.exports = function(source) { // 获取配置参数 const options = loaderUtils.getOptions(this); // 使用参数替换字符串 return source.replace('kkb', options.name || '默认值'); };传递参数:
module: { rules: [ { test: /\.js$/, use: { loader: path.resolve(__dirname, './loader/replaceLoader.js'), options: { name: '222' // 自定义参数 } } } ] }
1.3 处理异步操作
使用 this.async() 处理异步转换逻辑(如网络请求、文件读写)。
const loaderUtils = require('loader-utils');
module.exports = function(source) {
const options = loaderUtils.getOptions(this);
// 获取异步回调函数
const callback = this.async();
// 模拟异步操作(如读取文件)
setTimeout(() => {
const result = source.replace('kkb', options.name);
// 异步返回结果(err, content, sourceMap, meta)
callback(null, result);
}, 1000);
};
1.4 多 Loader 协作
Loader 执行顺序为从右到左(或从下到上),可串联处理模块。
创建第二个 Loader(
replaceLoaderAsync.js):module.exports = function(source) { return source.replace('222', 'Webpack 课程'); };配置 Loader 链:
module: { rules: [ { test: /\.js$/, use: [ './loader/replaceLoader.js', // 后执行 { loader: './loader/replaceLoaderAsync.js', options: { name: '222' } // 先执行 } ] } ] }简化 Loader 路径:
// 配置 Loader 查找目录 resolveLoader: { modules: ['node_modules', './loader'] // 优先查找自定义目录 }, module: { rules: [ { test: /\.js$/, use: ['replaceLoader', 'replaceLoaderAsync'] // 直接使用文件名 } ] }
1.5 Loader 核心 API
this.query:获取配置参数(简化版)this.callback():返回多值结果(内容、sourceMap 等)this.async():标记异步处理this.resourcePath:当前处理文件的路径- 更多 API 参考:Webpack Loader API
二、自定义 Webpack Plugin
Plugin 用于扩展 Webpack 功能,基于事件驱动模型,可在打包过程的特定时机执行任务。
2.1 Plugin 基础结构
Plugin 是一个类,必须包含 apply 方法,接收 compiler 实例作为参数。
// ./plugin/copyright-webpack-plugin.js
class CopyrightWebpackPlugin {
// 接收插件配置参数
constructor(options) {
this.options = options;
}
// 插件入口方法
apply(compiler) {
// compiler 是 Webpack 实例,包含所有配置信息
console.log('插件配置:', this.options);
}
}
module.exports = CopyrightWebpackPlugin;
使用插件:
const CopyrightWebpackPlugin = require('./plugin/copyright-webpack-plugin');
module.exports = {
plugins: [
new CopyrightWebpackPlugin({
name: '222' // 传递参数
})
]
};
2.2 监听 Webpack 生命周期
通过 compiler.hooks 注册事件回调,在特定阶段执行逻辑。
class CopyrightWebpackPlugin {
apply(compiler) {
// 同步钩子:编译开始时触发
compiler.hooks.compile.tap('CopyrightWebpackPlugin', () => {
console.log('编译开始了!');
});
// 异步钩子:资源输出前触发
compiler.hooks.emit.tapAsync(
'CopyrightWebpackPlugin',
(compilation, callback) => {
// compilation 包含当前构建的所有资源
compilation.assets['copyright.txt'] = {
source: () => '版权所有:222', // 文件内容
size: () => 12 // 文件大小(字节)
};
callback(); // 异步完成
}
);
}
}
2.3 核心概念
- Compiler:Webpack 主引擎,包含全局配置,生命周期贯穿整个打包过程
- Compilation:单次构建的上下文对象,包含当前构建的所有资源和依赖
- Hooks:事件钩子,如
compile(开始编译)、emit(输出资源)、done(完成构建)
常用钩子参考:Webpack Compiler Hooks
三、Webpack 打包原理分析
Webpack 本质是一个模块打包器,核心流程为:解析依赖 → 转换代码 → 合并输出。
3.1 打包核心流程
- 解析入口文件:从
entry开始,分析模块依赖 - 构建依赖图:递归解析所有模块的依赖关系
- 转换代码:通过 Loader 处理非 JS 模块,将所有模块转为 JS
- 生成输出:将转换后的模块合并为
bundle文件,实现自定义模块化系统
3.2 简化的打包产物
Webpack 最终生成的代码包含一个模块化加载器和所有模块的集合:
// 自执行函数包裹所有模块
(function(modules) {
// 缓存已加载的模块
var installedModules = {};
// 自定义 require 函数
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建新模块并缓存
var module = (installedModules[moduleId] = {
i: moduleId, // 模块 ID
l: false, // 是否已加载
exports: {} // 模块导出对象
});
// 执行模块代码
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
module.l = true; // 标记为已加载
return module.exports;
}
// 启动入口模块
return __webpack_require__('./src/index.js');
})({
// 模块集合:key 为模块路径,value 为模块代码
'./src/index.js': function(module, exports, __webpack_require__) {
eval('console.log("hello webpack");\n\n//# sourceURL=webpack:///./src/index.js?');
}
});
四、手动实现简易打包工具
通过模拟 Webpack 核心流程,实现一个简易的模块打包器。
4.1 步骤 1:解析单个模块
使用 @babel/parser 解析代码生成 AST,@babel/traverse 提取依赖,@babel/core 转换代码。
安装依赖:
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env -D
解析函数:
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');
// 分析单个模块
function moduleAnalyser(filename) {
// 1. 读取文件内容
const content = fs.readFileSync(filename, 'utf-8');
// 2. 解析为 AST
const ast = parser.parse(content, {
sourceType: 'module' // 解析 ES6 模块
});
// 3. 提取依赖
const dependencies = {};
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
const newPath = './' + path.join(dirname, node.source.value);
dependencies[node.source.value] = newPath; // 保存依赖路径映射
}
});
// 4. 转换为浏览器可执行代码
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
});
return {
filename,
dependencies,
code
};
}
4.2 步骤 2:构建依赖图
递归解析所有模块,生成完整的依赖关系图。
function makeDependenciesGraph(entry) {
const entryModule = moduleAnalyser(entry);
const graphArray = [entryModule];
// 递归解析所有依赖
for (let i = 0; i < graphArray.length; i++) {
const module = graphArray[i];
const { dependencies } = module;
if (dependencies) {
for (const key in dependencies) {
graphArray.push(moduleAnalyser(dependencies[key]));
}
}
}
// 转换为对象格式
const graph = {};
graphArray.forEach(item => {
graph[item.filename] = {
dependencies: item.dependencies,
code: item.code
};
});
return graph;
}
4.3 步骤 3:生成可执行代码
模拟 Webpack 的模块化加载器,生成最终可在浏览器运行的代码。
function generateCode(entry) {
const graph = JSON.stringify(makeDependenciesGraph(entry));
return `
(function(graph) {
// 缓存已加载模块
var modules = {};
function require(moduleId) {
if (modules[moduleId]) {
return modules[moduleId].exports;
}
// 定义模块
var module = (modules[moduleId] = {
exports: {}
});
// 执行模块代码
(function(require, exports, code) {
eval(code);
})(
// 局部 require,处理相对路径
function(localRequire) {
return require(graph[moduleId].dependencies[localRequire]);
},
module.exports,
graph[moduleId].code
);
return module.exports;
}
// 启动入口模块
require('${entry}');
})(${graph});
`;
}
4.4 执行打包
// 生成打包代码
const code = generateCode('./src/index.js');
// 输出到文件
fs.writeFileSync('./dist/bundle.js', code);
总结
- Loader:用于转换模块代码,是函数,执行顺序为从右到左
- Plugin:用于扩展 Webpack 功能,是类,基于事件钩子工作
- 打包原理:解析依赖 → 构建依赖图 → 转换代码 → 生成可执行 bundle
